你自己的 error 您所在的位置:网站首页 error code的翻译 你自己的 error

你自己的 error

#你自己的 error| 来源: 网络整理| 查看: 265

本系列文章翻译自 Andrzej’s C++ blog,原文链接。 翻译工作已获原作者授权!

系列文章目录

一、你自己的 error_code 二、你自己的 error_condition 三、高效地使用 error_code 四、关于 error_code 的一些澄清

文章目录 系列文章目录前言问题std::error_code接入自定义枚举定义错误类别致谢

前言

最近我在用std::error_code给自己的应用实现“错误状态分类”功能,这里分享一下我的一些经验和见解。

C++11 提供了一个非常精妙的错误状态分类机制,你也许已经见过一些相关的名词,比如“error code”、“error condition”、“error category”,但要搞清楚它们是干嘛的以及该怎么使用它们还是有点困难的。网上比较有价值的资料是 Boost.Asio 库的作者 Christopher Kohlhoff 所写的系列文章:

System error support in C++0x – part 1System error support in C++0x – part 2System error support in C++0x – part 3System error support in C++0x – part 4System error support in C++0x – part 5

这些文章已经写的很棒了,但我觉得能够提供更多的信息、提供另一种理解问题的思路,也是一件不错的事情,所以我们开始吧…

问题

首先,为什么我们需要错误码。假设我有一个航班查询系统,你告诉我从哪里出发到哪里,我就告诉你具体的航班以及价格。为了实现这个功能,我的系统需要调用另外两个服务:

一个查询并返回符合你要求的航班列表一个查询这个列表中的航班是否有空余的座位(以及是经济舱还是商务舱)

这里的每个服务都可能会因为很多原因而失败(产生错误),我们可以枚举失败的原因(每个服务都不一样)。比如,实现这两个服务的开发者们选择了这些枚举值:

enum class FlightsErrc { // no 0 NonexistentLocations = 10, // requested airport doesn't exist DatesInThePast, // booking flight for yesterday InvertedDates, // returning before departure NoFlightsFound = 20, // did not find any combination ProtocolViolation = 30, // e.g., bad XML ConnectionError, // could not connect to server ResourceError, // service run short of resources Timeout, // did not respond in time }; enum class SeatsErrc { // no 0 InvalidRequest = 1, // e.g., bad XML CouldNotConnect, // could not connect to server InternalError, // service run short of resources NoResponse, // did not respond in time NonexistentClass, // requested class does not exist NoSeatAvailable, // all seats booked };

可以看到,首先不同服务中错误的原因看起来很相像,但它们却被分配了不同的名字和数值(错误码),这是因为两个服务是由两个不同的团队独立开发的。而这也意味着同一个错误码可以表示两个完全不同的错误状态,取决于这个错误码是由哪个服务上报的。

其次,从枚举名中可以看出,导致错误的原因有如下几个不同来源:

环境:服务内部的问题(比如资源问题)通信:服务之间的通信用户:在请求中提供了错误的数据只是运气不好:其实算不上错误,但无法给用户响应(比如所有座位都被订了)

那么,我们到底为什么需要这些不同的错误码?当这其中任意一个错误发生时,我们会停止处理用户当前的请求,并给予一定反馈(帮助用户进一步操作)。如果系统无法给用户提供其请求的航班,我们想告诉用户以下几种情况:

你的请求无效没有任何已知的航线符合你的旅行需求系统内部出现了一些你无法理解的问题,这些问题导致我们无法给你请求的结果

另一方面,为了内部审查或者排查 bug,我们需要更详细的信息写入到日志中,比如错误是哪个系统上报的,及其产生的详细原因。这些可以编码成一个整形数字,其它更多的信息(比如我们想连接的是哪个机站,或者我们尝试接入的是哪个数据库)可以被分别记录在日志中,所以用整形数字编码错误类型已经足够了。

std::error_code

C++ 标准库中的std::error_code正是被设计来表示这样的信息:一个数字代指状态,加上这个数字表示的含义所在的“域”。换而言之,std::error_code是一个元素对(pair):{int, domain},这体现在它的接口中:

void inspect(std::error_code ec) { ec.value(); // the int value ec.category(); // the domain }

但你基本上不会用这种方式来检验一个 error_code。就像之前所述,我们想做的是两件事:将最原始的 error_code(未被后续的上层应用解析)记录下来;以及用它来回答特定的问题,比如“这个错误是不是因为用户提供了他明知有误的信息导致的”。

如果你问,为什么要用std::error_code而不是异常(exception)呢?我得在此澄清一下:这两者并不是相互排斥的。我会在程序中使用异常来报告错误,而异常内部会包含一个 error_code,用于方便地检验错误信息(而不是通过储存和解析字符串来实现)。std::error_code跟避免异常一点关系都没有!另外,我并不觉得程序中很需要特别多的异常类型,往往一个类型就够了:我会在一个(或两个)地方统一 catch 它们,然后检验 error_code 来区分不同的错误状态。

译者注:std::filesystem就是这样使用 error_code 的,其函数提供两个版本:一个将错误码作为函数参数返回;另一个在出现错误时将错误码作为异常抛出。参见 std::filesystem::copy。

接入自定义枚举

现在我们要调整一下std::error_code,让它能够存放前述 Flights 服务的各种错误状态:

enum class FlightsErrc { // no 0 NonexistentLocations = 10, // requested airport doesn't exist DatesInThePast, // booking flight for yesterday InvertedDates, // returning before departure NoFlightsFound = 20, // did not find any combination ProtocolViolation = 30, // e.g., bad XML ConnectionError, // could not connect to server ResourceError, // service run short of resources Timeout, // did not respond in time };

我们需要能够将自己的枚举值转化为std::error_code:

std::error_code ec = FlightsErrc::NonexistentLocations;

这里要注意的是,我们的枚举值必须满足一个条件:数值 0 不能用来表示错误状态。0 在任何错误域(类别)中都是用来表示“成功”(无错误)的,这一特例在之后我们检验std::error_code的时候能够利用上:

void inspect(std::error_code ec) { if (ec) // equivalent to: ec.value() != 0 handle_failure(ec); else handle_success(); }

从这个意义上来说,此博文使用数值 200 表示成功其实是不对的。

所以我们的FlightsErrc枚举值不从 0 开始。这同时也导致我们可以定义不属于任意一个枚举元素的枚举值:

FlightsErrc fe {};

这是 C++ 枚举类型(甚至 C++11 中的强类型枚举 enum class)的一个重要性质:你能够在枚举元素定义的范围外创建数值。正因为如此,编译器会在switch语句中报出“不是所有的控制路径都返回值(not all control paths return value)”的警告,即使你给所有的枚举元素都写了case。

回到错误码的转换上,std::error_code有一个转换构造模板函数,看起来就像这样:

template requires is_error_code::value error_code(Errc e) noexcept : error_code{make_error_code(e)} {}

(当然,我用了一个还不存在的 concepts 语法,但你可以 get 到我的意思:只有当std::is_error_code::value为true的时候,这个构造函数才可用)

译者注:concepts 语法由 C++20 标准引入,本博文原文发布于 2017 年,当时还没有 C++20 标准。

该构造函数就是一个能够将自定义枚举接入 error_code 系统的“定制钩子(customization hook)”。想要接入FlightsErrc,我们需要确保:

std::is_error_code::value返回true;以FlightsErrc为输入参数的make_error_code()函数被定义并且能够被访问。

关于第一点,我们需要指定一个标准类型特性:

namespace std { template struct is_error_code_enum : true_type {}; }

这是在std命名空间中进行声明的合法情况之一。

关于第二点,我们只需要在枚举类型FlightsErrc相同的命名空间中重载make_error_code函数即可:

enum class FlightsErrc; std::error_code make_error_code(FlightsErrc);

这些是程序或者库的其它部分需要看到内容,因此我们必须把它们放在头文件中。至于make_error_code函数的具体实现,我们可以放在另一个独立的翻译单元(cpp 文件)中。

完成以上内容后,我们便可以认为FlightsErrc就是一种error_code啦:

std::error_code ec = FlightsErrc::NoFlightsFound; assert (ec == FlightsErrc::NoFlightsFound); assert (ec != FlightsErrc::InvertedDates); 定义错误类别

至此,我还只说过error_code是一个元素对:{number, domain},其中第一个元素在某个域中唯一确定了一种错误情况,第二个元素则在所有可能的错误域中唯一确定了一个域。问题是,域的唯一标识(ID)需要用一个机器单词来存放,我们怎么才能确保它在现有以及未来的所有库中都是独一无二的呢?我们将域 ID 作为一个实现细节隐藏了起来,如果我们想使用另一个有着自己的错误枚举的第三方库,怎么才能确保它们的域 ID 与我们不同?

std::error_code选用的解决方法是基于这样的事实:每一个全局对象(或者用一种更正式的方式来说:命名空间作用域内的对象)都被分配了一个独一无二的地址。不管将多少库组合在一起,不管有多少全局对象,每个全局对象都有一个独特的地址 —— 这很显然。

为了利用这一点,我们可以将每个想要插入到 error_code 系统中的类型与一个独特的全局对象关联起来,用该对象的地址作为 ID。也就是说,可以使用一个指针来代表域,而这正是std::error_code所采用的方法。现在的问题是,我们用作 ID 的这个指针T*具体是什么一种对象T?有一个很聪明的选择:我们用一种能够提供额外好处的类型。具体使用的类型T是std::error_category,其额外的好处体现在它的接口中:

class error_category { public: virtual const char* name() const noexcept = 0; virtual string message(int ev) const = 0; // other members ... };

我在之前用了一个叫“域(domain)”的名字,而标准库称其为“错误类别”,表达的是同一个意思。

它有纯虚成员函数,这暗示我们用对象指针作域 ID 的类需要从std::error_category派生而来,而且每一个错误枚举类型都需要从std::error_category派生一个新的相关的类。通常,有着纯虚函数的类意味着在堆上创建对象,但我们不这么做。我们要创建全局对象,然后用指针指向它们。

译者注:带有纯虚函数的类又叫抽象类,本身无法实例化。很常见的一种用法是:在抽象类中定义一系列接口(纯虚函数),在其派生类中实现这些接口,然后具体用的时候 new 一个派生类对象(堆上),通过基类的指针指向派生类并访问相关函数。这就是为什么作者说有纯虚函数通常意味着在堆上创建对象。

std::error_category中还有一些别的虚函数,这些函数在其它有些情况下需要定制(重载),但我们的目的只是把 FlightsErrc 插入到 error_code 系统中,所以不需要考虑它们。

现在,对于每一个派生自std::error_category用于表示错误域的类,我们需要提供两个成员函数。函数 name 应该返回一个标识这个错误类别(域)的名称。函数 message 则对该错误域中每个枚举值指定一个文本描述。为了更好地阐述,我们给枚举类型 FlightsErrc 定义一个错误类别。记住:这个从std::error_category派生而来的类只需要在一个翻译单元中可见即可,其它文件中我们只需要用到它的一个实例的地址。

namespace { // anonymous namespace struct FlightsErrCategory : std::error_category { const char* name() const noexcept override; std::string message(int ev) const override; }; const char* FlightsErrCategory::name() const noexcept { return "flights"; } std::string FlightsErrCategory::message(int ev) const { switch (static_cast(ev)) { case FlightsErrc::NonexistentLocations: return "nonexistent airport name in request"; case FlightsErrc::DatesInThePast: return "request for a date from the past"; case FlightsErrc::InvertedDates: return "requested flight return date before departure date"; case FlightsErrc::NoFlightsFound: return "no filight combination found"; case FlightsErrc::ProtocolViolation: return "received malformed request"; case FlightsErrc::ConnectionError: return "could not connect to server"; case FlightsErrc::ResourceError: return "insufficient resources"; case FlightsErrc::Timeout: return "processing timed out"; default: return "(unrecognized error)"; } } const FlightsErrCategory theFlightsErrCategory {}; }

函数name()返回错误类别的名称,可以输出到一些比如日志之类的东西中:它能帮助你定位错误的原因。这对于不同的错误枚举并不要求独一无二,但不恰当的名称可能导致日志文件的歧义。

函数message()为错误类别中的每个枚举值提供了一段描述信息。它在你调试或者浏览日志时很有用;但你或许并不想将它未经处理就提供给用户。这些信息跟我们在一开始写在FlightsErrc定义中的注释很接近。

message()通常是被间接调用的,调用者无法知道错误码数值是一个FlightsErrc,所以我们需要显式的将其转化为FlightsErrc。我敢肯定前面提到的这个博文因为遗漏了static_cast而无法编译。经过转化之后,还存在被检验的数值不在枚举序列中的风险,因此我们需要default标签。(有趣的是,每当我在程序中用到enum class,我就立刻发现自己需要用static_cast将其与int来回转化)

最后可以注意到,我们初始化了一个全局的FlightsErrCategory对象,这是整个程序中唯一一个该类型的对象。我们需要它的地址(来区分不同错误类别中的error_code),同时也会用到它的多态性质。

虽然类std::error_category不是一个字面类型 (literal type),但它有一个constexpr属性的默认构造函数。而FlightsErrCategory的隐式声明的默认构造函数也继承了这个constexpr属性,所以它的全局对象是常量初始化(constant initialization),参见这篇博文,因此能够避免 static initialization order fiasco。

至此,最后缺少的部分只剩下make_error_code()函数的实现:

std::error_code make_error_code(FlightsErrc e) { return {static_cast(e), theFlightsErrCategory}; }

大功告成,我们的FlightsErrc可以被当成std::error_code来用了:

int main() { std::error_code ec = FlightsErrc::NoFlightsFound; std::cout


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有